package com.gusto.engine.clusterant.algorithms.kmeansclustering;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import com.gusto.engine.clusterant.access.ClusterDAO;
import com.gusto.engine.clusterant.model.Point;

/**
 * <p>KMeans clustering algorithm</p>
 * 
 * @author amokrane.belloui@gmail.com
 *
 */
public class KMeansClustering {
	 
	private Logger log = Logger.getLogger(getClass());
	
	private int ROWS = 18;
	private int COLS = 18;
	private int CLUSTER_COUNT = 5;
	private double QUALITY_THRESHOLD = 1.0;
	private int MAX_ITERATIONS = 20;
	 
	private ClusterDAO clusterDAO;
	public void setClusterDAO(ClusterDAO clusterDAO) {
		this.clusterDAO = clusterDAO;
	}
	
	private enum ClusterType {
		EMPTY, GOOD, WORSE
	}
	
	private Map<ClusterType, List<Cluster>> classifyClusters(List<Cluster> clusters) {
		Map<ClusterType, List<Cluster>> classificationClusters = new HashMap<ClusterType, List<Cluster>>();
		classificationClusters.put(ClusterType.EMPTY, new ArrayList<Cluster>());
		classificationClusters.put(ClusterType.GOOD, new ArrayList<Cluster>());
		classificationClusters.put(ClusterType.WORSE, new ArrayList<Cluster>());
		
		Cluster worseCluster = null;
		double worseDistance = 0.0;
		
		for (Cluster c : clusters) {
			if (c.isEmpty()) {
				classificationClusters.get(ClusterType.EMPTY).add(c);
			}
			else {
				classificationClusters.get(ClusterType.GOOD).add(c);
				
				double quality = c.getQuality();
				if (quality > QUALITY_THRESHOLD) {
					if (quality > worseDistance) {
						worseCluster = c;
						worseDistance = quality;
					}
				}
			}
		}
		if (worseCluster != null) {
			classificationClusters.get(ClusterType.GOOD).remove(worseCluster);
			classificationClusters.get(ClusterType.WORSE).add(worseCluster);
		}
		
		return classificationClusters;
	}
	
	public class ClusterPair {
		
		private Cluster cluster1;
		private Cluster cluster2;
		
		public ClusterPair() {
			super();
		}
		
		public ClusterPair(Cluster cluster1, Cluster cluster2) {
			super();
			this.cluster1 = cluster1;
			this.cluster2 = cluster2;
		}
		
		public Cluster getCluster1() {
			return cluster1;
		}
		public void setCluster1(Cluster cluster1) {
			this.cluster1 = cluster1;
		}
		public Cluster getCluster2() {
			return cluster2;
		}
		public void setCluster2(Cluster cluster2) {
			this.cluster2 = cluster2;
		}
		
		public Double getDistance() {
			if (cluster1 != null && cluster2 != null) {
				double d = Math.sqrt( 
						Math.pow(cluster1.getRow() - cluster2.getRow(), 2) + 
						Math.pow(cluster1.getCol() - cluster2.getCol(), 2) 
				);
				return d;
			}
			else {
				return Double.POSITIVE_INFINITY;
			}
		}
		
		public Cluster merge() {
			Cluster cluster = new Cluster(0, cluster1.getRow(), cluster1.getCol());
			cluster.getComponents().addAll(cluster1.getComponents());
			cluster.getComponents().addAll(cluster2.getComponents());
			cluster.computeCenter();
			return cluster;
		}
		
	}
	
	public ClusterPair getClosestClusters(List<Cluster> clusters) {
		//Double closestDistance = Double.POSITIVE_INFINITY;
		ClusterPair closestPair = new ClusterPair();
		
		for (Cluster c1 : clusters) {
			for (Cluster c2 : clusters) {
				if (c1 != c2) {
					ClusterPair p = new ClusterPair(c1, c2);
					if (p.getDistance() < closestPair.getDistance()) {
						//closestDistance = p.getDistance();
						closestPair = p;
					}
				}
			}
		}
		
		return closestPair;
	}
	
	/**
	 * Clusters a set of {@link Point}s in to a set of {@link Cluster}s 
	 * @param points
	 * @return
	 */
	public List<Cluster> cluster(List<Point> points) {
		log.info("Clustering " + points.size() + " points");
		List<Cluster> clusters = cluster(points, CLUSTER_COUNT);
		
		Map<ClusterType, List<Cluster>> cc = classifyClusters(clusters);
		
		//Double closestDistanceBefore = Double.POSITIVE_INFINITY;
		//Double closestDistanceAfter = Double.POSITIVE_INFINITY;
		//Double closestDistance = 0.0;
		
		while (
				(cc.get(ClusterType.EMPTY).size() > 0) &&
				(cc.get(ClusterType.WORSE).size() > 0)
		) {
			Cluster worseCluster = cc.get(ClusterType.WORSE).get(0);
			List<Cluster> newClusters = cluster(worseCluster.getComponents(), cc.get(ClusterType.EMPTY).size() + 1);
			
			clusters = cc.get(ClusterType.GOOD);
			clusters.addAll(newClusters);
			
			cc = classifyClusters(clusters);
		}
		
		
		ClusterPair pair = getClosestClusters(clusters);
		log.info("About to merge 2 clusters into global " + pair.merge() + ". Quality is: " + pair.merge().getQuality());
		
		while (pair.merge().getQuality() <= QUALITY_THRESHOLD) {
			for (Point p : pair.getCluster2().getComponents()) {
				pair.getCluster1().getComponents().add(p);
			}
			pair.getCluster2().emptyPoints();
			pair.getCluster2().computeCenter();
			pair.getCluster1().computeCenter();
			log.info("Merged into " + pair.getCluster1());
			
			pair = getClosestClusters(clusters);
			log.info("About to merge 2 clusters into global " + pair.merge() + ". Quality is: " + pair.merge().getQuality());
		}
		
		for (Cluster c : clusters) {
			log.info(c + " " + c.getRow() + "-" + c.getCol() + " " + c.getQuality());
		}
		
		return clusters;
	}
	
	private List<Cluster> cluster(List<Point> points, int nbClusters) {
		List<Cluster> theClusters = new ArrayList<Cluster>();
		log.info("About to cluster " + points.size() + " points in " + nbClusters + " clusters");
		for (int k = 0; k < nbClusters; k++) { 
			Double row = 1.0 * Integer.parseInt(Math.round(Math.random() * (ROWS-1)) + "");
			Double col = 1.0 * Integer.parseInt(Math.round(Math.random() * (COLS-1)) + "");
			theClusters.add(new Cluster(k, row, col));
		}
		
		int nb = 0;
		boolean contine = true;
		while (contine) {
			affectPointsToClusters(points, theClusters);
			boolean changed = false;
			for (Cluster cluster : theClusters) {
				Double oldRow = cluster.getRow();
				Double oldCol = cluster.getCol();
				cluster.computeCenter();
				if (!changed) {	
					if ( !(oldRow.equals(Double.NaN) && cluster.getRow().equals(Double.NaN)) ) {
						if (!oldRow.equals(cluster.getRow())) {
							changed = true;
						}
					}
					if ( !(oldCol.equals(Double.NaN) && cluster.getCol().equals(Double.NaN)) ) {
						if (!oldCol.equals(cluster.getCol())) {
							changed = true;
						}
					}
				}
				cluster.emptyPoints();
			}
			if (!changed) {
				log.debug("Clusters haven't change between iterations. Done in " + nb + " iterations");
			}
			if (nb >= MAX_ITERATIONS) {
				log.debug("Maximal number of iterations " + MAX_ITERATIONS + " reached");
			}
			nb++;
			if (nb >= MAX_ITERATIONS || !changed) {
				contine = false;
			}
		}
		
		affectPointsToClusters(points, theClusters);
		for (Cluster cluster : theClusters) {
			cluster.computeCenter();
		}
		return theClusters;
	}
	
	private void affectPointsToClusters(List<Point> points, List<Cluster> theClusters) {
		log.debug("Affecting points to clusters");
		if (clusterDAO != null) {
			clusterDAO.removeAllObjects();
		}
		for (Point p : points) {
			//Classify point
			Cluster bestCluster = null;
			Double bestDistance = Double.POSITIVE_INFINITY;
			for (Cluster c : theClusters) {
				Double ed = 
					Math.sqrt( 
						Math.pow(c.getRow() - p.getRow(), 2) + 
						Math.pow(c.getCol() - p.getCol(), 2) 
					);
				
				if (ed.doubleValue() < bestDistance.doubleValue()) {
					bestDistance = ed;
					bestCluster = c;
				}
			}
			bestCluster.getComponents().add(p);
			if (clusterDAO != null) {
				clusterDAO.addObjectToCluster(bestCluster.getId(), p.getRow(), p.getCol(), p.getObject());
			}
		}
	}

	public int getROWS() {
		return ROWS;
	}

	public void setROWS(int rows) {
		ROWS = rows;
	}

	public int getCOLS() {
		return COLS;
	}

	public void setCOLS(int cols) {
		COLS = cols;
	}

	public int getCLUSTER_COUNT() {
		return CLUSTER_COUNT;
	}

	public void setCLUSTER_COUNT(int cluster_count) {
		CLUSTER_COUNT = cluster_count;
	}

	public double getQUALITY_THRESHOLD() {
		return QUALITY_THRESHOLD;
	}

	public void setQUALITY_THRESHOLD(double quality_threshold) {
		QUALITY_THRESHOLD = quality_threshold;
	}

	public int getMAX_ITERATIONS() {
		return MAX_ITERATIONS;
	}

	public void setMAX_ITERATIONS(int max_iterations) {
		MAX_ITERATIONS = max_iterations;
	}
	
}
